package pnt;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;


public class PNTMessage {
    
    private static Logger logger = Logger.getLogger("pnt");
    
    Map<Attribute, String> values;
    String version;
    
    boolean isRequest;
    
    public RequestType request;
    public String requestAttribute;
    public ResponseType response;
    
    static int LAST_TRANSACTION = 0;
    
/** Constructors **/
    
    /**
     * Creates a PNT message from raw-text.
     * 
     * FORMAT:
     * -------
     * PNT Messages headers take either:
     *     the request format  "<Request>     *|<target resource>|<cookie no.>        PNT/1.0"
     *  or the response format "PNT/1.0 <response code> <response message in ASCII>         "
     * they must have at least 3 whitespace separated components.
     * 
     * PNT message attributes are given one per line. Consisting of the attribute 
     * name followed by the attribute value and separated with a colon.
     * 
     * The message is terminated by the code "<CR><LF><CR><LF>".
     * -------
     * 
     * @param s
     * the message text.
     * @throws pnt.PNTMessage.MalformedPacketException
     * is thrown where the packet is malformed or contains unrecognised elements.
     */
    public PNTMessage(String s) throws MalformedPacketException{
        values = new TreeMap<Attribute, String>();
        
        String[] lines = s.split("\n");
        // Make sure there's enough lines to constitute a packet
        if (lines.length <= 1 )
            throw new MalformedPacketException("Insufficient Attributes. MSG:" + s );
        
        
        /** Process Header (can this be done more efficiently with regex?) **/
        String header = lines[0];
        // Remove comments
        if (header.indexOf(";") != -1) { header = header.substring(0, header.indexOf(";"));}
        
        String[] headerAtoms = header.split("[,\\s]+");
        
        if (headerAtoms.length < 3)
             throw new MalformedPacketException("Malformed msg header: " + header);
        
        if (headerAtoms[0].equalsIgnoreCase("PNT/1.0")){
        // Packet is a response    
            
            isRequest = false;
            this.response = ResponseType.parseResponseType(Integer.parseInt(headerAtoms[1]));
            if (response == ResponseType.UNKNOWN)
                throw new MalformedPacketException("Unknown Response Type: " + header);

        } else if (headerAtoms[headerAtoms.length - 1].equalsIgnoreCase("PNT/1.0")){
        // Packet is a request
            
            if (headerAtoms.length != 3 )
                throw new MalformedPacketException("Malformed request (needs exactly 3 words)" + s );
            
            isRequest = true;
            this.request = RequestType.parseRequestType(headerAtoms[0]);
            if (request == RequestType.UNKNOWN)
                throw new MalformedPacketException("Unknown Request Type: " + header);
            
            this.requestAttribute = headerAtoms[1];
            
        } else {
            throw new MalformedPacketException("Malformed packet header, or incompatable protocol version: " + header);
        }
        
        
        /** Process Body **/
        for(int i = 1; i < lines.length; i++){
            String ln = lines[i];
            
            // verify format
            int colon = ln.indexOf(':');
            if(i < 1)
                throw new MalformedPacketException("Malformed attribute: " + ln);
            
            // strip comments
            int com = ln.indexOf(';');
            if (com > 0)
                ln = ln.substring(0, com - 1);
            
            String type = ln.substring(0, colon);
            String value = ln.substring(colon + 1, ln.length());
            values.put(Attribute.parseAttribute(type.trim()), value.trim());
        }
        logger.log(Level.FINER, "Message Parsed", this );
        
        /** Verify transaction number **/
        if (this.getTransactionNumber() == -1)
            throw new MalformedPacketException("Malformed or non-existant transaction number.");
        
    }
    
    public PNTMessage(byte[] b) throws MalformedPacketException { this(new String(b)); }
    
    public PNTMessage(DatagramPacket d)  throws MalformedPacketException { this(d.getData()); }
    

    /**
     * Creates a new request message
     * 
     * @param r The type of the request (ALLOCATE-HEP etc)
     * @param attribute Any optional parameters for the header. If left blank or
     * null this defaults to 
     */
    public PNTMessage(RequestType r, String attribute){
        values = new TreeMap<Attribute, String>();
        this.isRequest = true;
        this.request = r;
        if(attribute == null ||(attribute.trim()).compareTo("") == 0)
            attribute = "*";
        this.requestAttribute = attribute;
    }
    
    /**
     * Creates a new response message
     * 
     * @param r The type of response.
     */
    public PNTMessage(ResponseType r){
        values = new TreeMap<Attribute, String>();
        this.isRequest = false;
        this.response = r;
    }
    
/** Converter functions**/
    
    public String toString(){
        String message;
        
        // Create header
        if (this.request != null)
            message = request.name() + 
                      "          " + 
                      this.requestAttribute + 
                      "          " +
                      "PNT/1.0" +
                      "\n";
            
        else if (this.response != null)
            message = "PNT/1.0 " + 
                      response.getCode() + 
                      " " + 
                      response.getMessage() + 
                      "\n";
            
        else
            throw new Error ("MESSAGE IS NEITHER REQUEST OR RESPONSE");
        
        // add body attributes
        
        for (Iterator<Attribute> iter = values.keySet().iterator(); iter.hasNext();){
            Attribute field = (Attribute) iter.next();
            String value = values.get(field);
            
            message = message + field.getName() + ": " + value + "\n";
            
        }
        
        message = message + "\n";
        
        return message;
    }
    
    public byte[] getBytes(){
        return toString().getBytes();
    }
    
    public void print(){
        System.out.println(toString());
    }

/** Attribute centric and Descriptive functions **/

    public boolean isRequest(){
        return isRequest;
    }
    
    public void insertAttribute(Attribute type, String value){
        if(value != null && value.length() != 0) values.put(type, value);
    }
    
    public String getAttribute(Attribute a){
        return values.get(a);
    }
    
    public boolean hasAttribute(Attribute a){
        return values.containsKey(a);
    }
    
    public Collection <String> getValues(){
        return values.values();
    }
   
/** Enums and Classes **/
    
    public static enum RequestType{ 
        ALLOCATEHEP, DISCOVERNEP, REGISTER, DEREGISTER, UNKNOWN;
        
        
        public static RequestType parseRequestType(String s){
            for (RequestType a : RequestType.values()){
                if (("" + a).compareTo(s) == 0)
                    return a;
            }
            return UNKNOWN;
        }
    
    }
    
    public static enum ResponseType{
        
        OK(200, "OK"), 
        CONNECTION_CREATED(201, "Connection Created"),
        LOGIN_REQUIRED(401, "Login Required"), 
        TARGET_USER_UNAVAILABLE(402, "Target User Unavailable"),
        PNT_VERSION_NOT_SUPPORTED(403, "PNT Version Not Supported"),
        SERVER_UNABLE_TO_MATCH_COOKIE(404, "Server Unable To Match Cookie"),
        SESSION_DISALLOWED(405, "Session Disallowed"),
        RELAY_ENDPOINT_ALREADY_EXISTS(406, "Relay Endpoint Already Exists"),
        OUT_OF_RESOURCES(407, "Out Of Resources"),
        UNABLE_TO_SETUP_CONNECTION_WITH_REMOTE_NODE(408, "Unable To Setup Connection With Remote Node"),
        APPLICATION_UNABLE_TO_MATCH_THE_COOKIE(504, "Application Unable To Match The Cookie"),
        UNKNOWN (-1, "UNKNOWN CODE");
        
        String message;
        int code;
        
        ResponseType(int code, String message){
            this.message = message;
            this.code = code;
        }
        
        public static ResponseType parseResponseType(int i){
            for (ResponseType a : ResponseType.values())
                if (a.getCode() == i)
                    return a;
            return UNKNOWN;
        }
        
        public int getCode(){return code;}
        public String getMessage(){return message;}
    }
    
    public static enum Attribute{
        
        TRANSACTION("Transaction"), 
        FROM("From"), 
        HEPCOUNTREQUEST("Hep-Count-Request"),
        PVTENDPOINT("Pvt-Endpoint"),
        NATENDPOINT("NAT-Endpoint"),
        HELPERENDPOINTS("Helper-Endpoints"),
        USERAUTHREQUIRED("User-Auth-Required"),
        AUTHORIZATION("Authorization"),
        REGISTEREDWITH("Registered-with"),
        TO("To"),
        ENDUSER("End-User"),
        APPLICATION("Application"),
        INSTANCE("Instance"),
        PRIVATEENDPOINTS("Private-endpoints"),
        EXTENDPOINTS("Ext-endpoints"),
        EXPIRES("Expires"),
        UNKNOWNATTRIBUTE("Unknown");
        
        String name;
        
        Attribute(String name){
            this.name = name;
        }
        
        public static Attribute parseAttribute(String s){
            for (Attribute a : Attribute.values())
                if (a.getName().compareTo(s) == 0)
                    return a;
            return UNKNOWNATTRIBUTE;
        }
        
        public String getName(){ return name; }
    }

    public static class MalformedPacketException extends Exception {
		private static final long serialVersionUID = 1L;

		public MalformedPacketException(String s){
            super(s);
        }
    }

/** Static Hhelper functions **/
        /**
     * THIS WILL CURRENTLY BLOCK IF A MESSAGE IS NOT RETURNED!!!!!!!!
     * 
     * @return
     */
    public static PNTMessage getMessage(Socket sock) throws MalformedPacketException {
        
        try {
        
        BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
        
        String s = "";
        
        String temp;

        do {
            /**
             * Possibly include code here to check if "in" is ready. if not,
             * wait for a while then do again... keep a counter and throw exception
             * after x iterations.
             */
            temp = in.readLine();
            s = s + temp + "\n";
            
        } while (temp.compareTo("") != 0);
        
        logger.log(Level.FINEST, "Message received from " + sock.getInetAddress().getHostAddress() + ":" + sock.getPort(), s);
        
        return new PNTMessage(s);
        
        } catch (IOException ex) {
            System.err.println(ex);
            throw new MalformedPacketException("Unable to read from socket");
        }
        
    }
    
    public static PNTMessage allocate_hep(int hep_count_request, String from, String private_address){
        PNTMessage msg = new PNTMessage(RequestType.ALLOCATEHEP, null);
        
        msg.insertAttribute(Attribute.TRANSACTION, Integer.toString(++LAST_TRANSACTION));
        msg.insertAttribute(Attribute.HEPCOUNTREQUEST, Integer.toString(hep_count_request));
        msg.insertAttribute(Attribute.FROM, from);
        msg.insertAttribute(Attribute.PVTENDPOINT, private_address);
        
        return msg;
    }
    
    public static PNTMessage discover_nep(String from, String private_endpoint){
        PNTMessage msg = new PNTMessage(RequestType.DISCOVERNEP, null);
        
        msg.insertAttribute(Attribute.TRANSACTION, Integer.toString(++LAST_TRANSACTION));
        msg.insertAttribute(Attribute.FROM, from);
        msg.insertAttribute(Attribute.PVTENDPOINT, private_endpoint);
        
        return msg;
    }
    
    public static PNTMessage register(  boolean user_auth_required, 
                                        String authorization, 
                                        String registered_with,
                                        String from,
                                        String to,
                                        String end_user,
                                        String application,
                                        String instance,
                                        String private_endpoints,
                                        String ext_endpoints,
                                        String expires){
        PNTMessage msg = new PNTMessage(RequestType.REGISTER, null);
        msg.insertAttribute(Attribute.TRANSACTION, Integer.toString(++LAST_TRANSACTION));
        msg.insertAttribute(Attribute.USERAUTHREQUIRED, (user_auth_required ? "Yes" :"No"));
        msg.insertAttribute(Attribute.AUTHORIZATION, authorization);
        msg.insertAttribute(Attribute.REGISTEREDWITH, registered_with);
        msg.insertAttribute(Attribute.FROM, from);
        msg.insertAttribute(Attribute.TO, to);
        msg.insertAttribute(Attribute.ENDUSER, end_user);
        msg.insertAttribute(Attribute.APPLICATION, application);
        msg.insertAttribute(Attribute.INSTANCE, instance);
        msg.insertAttribute(Attribute.PRIVATEENDPOINTS, private_endpoints);
        msg.insertAttribute(Attribute.EXTENDPOINTS, ext_endpoints);
        msg.insertAttribute(Attribute.EXPIRES, expires);
            
        return msg;
    }
    
    public static PNTMessage deregister(String end_user, 
                                        String application, 
                                        String instance) {
        
        PNTMessage msg = new PNTMessage(RequestType.DEREGISTER, null);
        
        msg.insertAttribute(Attribute.TRANSACTION, Integer.toString(++LAST_TRANSACTION));
        msg.insertAttribute(Attribute.ENDUSER, end_user);
        msg.insertAttribute(Attribute.APPLICATION, application);
        msg.insertAttribute(Attribute.INSTANCE, instance);
        
        return msg;
    }

/** Parsing functions **/
    
    /**
     * Returns the transaction number of this message. 
     * @return The transaction number of this message.
     */
    public int getTransactionNumber() {
        // The -1 is verified upon parsing. If found, a MalformedPacketException
        // is thrown.
        if (!hasAttribute(Attribute.TRANSACTION))
            return -1;
        try{
            return Integer.parseInt(getAttribute(Attribute.TRANSACTION));
        } catch (NumberFormatException ex){
            return -1;
        }
    }

    
    public InetSocketAddress parseNATEndpoint() throws MalformedPacketException {
       if(!hasAttribute(Attribute.NATENDPOINT))
           throw new MalformedPacketException("Message does not contain PVTENDPOINT attribute");
       
       String pvtEndpoint = getAttribute(Attribute.NATENDPOINT);
       
       // remove braces
       pvtEndpoint = pvtEndpoint.substring(1, pvtEndpoint.length() - 1);
       
       return parseAddress(pvtEndpoint);
    }

    public InetSocketAddress[] parseHelperEndpoints() throws MalformedPacketException {
       if(!hasAttribute(Attribute.HELPERENDPOINTS))
           throw new MalformedPacketException("Message does not contain HELPERENDPOINTS attribute");
       
       String hlprEndpoints = getAttribute(Attribute.HELPERENDPOINTS);
       
       // remove leading and trailing braces
       hlprEndpoints = hlprEndpoints.substring(1, hlprEndpoints.length() - 1);
       
       String[] endpoints = hlprEndpoints.split("\\),\\(");

       
       InetSocketAddress[] endpointAddresses = new InetSocketAddress[endpoints.length];
       for( int i = 0; i < endpoints.length; i++)
           endpointAddresses[i] = parseAddress(endpoints[i]);
       
       return endpointAddresses;
    }
    
/** helper functions **/
    // parses an address in form "addr,port" i.e. without brackets
    private InetSocketAddress parseAddress(String s) throws MalformedPacketException {
       int separator = s.indexOf(',');
       
       try {
           
       int port = Integer.parseInt( s.substring(separator + 1, s.length()));
       InetAddress addr = Inet4Address.getByName(s.substring(0, separator));
       
       return new InetSocketAddress(addr, port);
       
       } catch (NumberFormatException ex){
           logger.warning("Unable to parse port");
           throw new MalformedPacketException("Malformed address attribute");
       } catch (UnknownHostException ex) {
            logger.warning("Unable to parse address");
            throw new MalformedPacketException("Malformed address attribute");
        }
    }
}
